package fhq.hcmute.qrcode.detector;

import static fhq.hcmute.qrcode.util.QRCodeException.INVALID_REGION;
import static fhq.hcmute.qrcode.util.QRCodeException.INVALID_VERSION;
import static fhq.hcmute.qrcode.util.QRCodeException.QR_CODE_NOT_FOUND;
import fhq.hcmute.qrcode.common.BinaryImage;
import fhq.hcmute.qrcode.common.BitMatrix;
import fhq.hcmute.qrcode.common.Version;
import fhq.hcmute.qrcode.detector.util.ImageTransform;
import fhq.hcmute.qrcode.detector.util.QRCodeSample;
import fhq.hcmute.qrcode.util.MathUtils;
import fhq.hcmute.qrcode.util.QRCodeException;

public class Detector {
	private BinaryImage image;

	public Detector(BinaryImage image) {
		this.image = image;
	}

	public BitMatrix detectBitMatrix() throws QRCodeException {
		FinderPatternFinder finder = new FinderPatternFinder(image);
		FinderPatternInfo finderPatternInfo = finder.find();
		if (finderPatternInfo == null) {
			throw new QRCodeException(QR_CODE_NOT_FOUND);
		}

		PatternPoint topLeft = finderPatternInfo.getTopLeft();
		PatternPoint topRight = finderPatternInfo.getTopRight();
		PatternPoint bottomLeft = finderPatternInfo.getBottomLeft();

		float moduleSize = calculateModuleSize(topLeft, topRight, bottomLeft);
		int version = determineVersion(topLeft, topRight, bottomLeft,
				moduleSize);
		int dimension = Version.getVersionByNumber(version).getModules();
		int modulesBetweenFPCenters = dimension - 7;
		PatternPoint alignmentPattern = null;
		if (version > 1) {
			float bottomRightX = topRight.getX() - topLeft.getX()
					+ bottomLeft.getX();
			float bottomRightY = topRight.getY() - topLeft.getY()
					+ bottomLeft.getY();
			float correctionToTopLeft = 1.0f - 3.0f / (float) modulesBetweenFPCenters;
			int estAlignmentX = (int) (topLeft.getX() + correctionToTopLeft
					* (bottomRightX - topLeft.getX()));
			int estAlignmentY = (int) (topLeft.getY() + correctionToTopLeft
					* (bottomRightY - topLeft.getY()));
			alignmentPattern = findAlignmentInRegion(moduleSize, estAlignmentX,
					estAlignmentY);
		}

		ImageTransform transform = createTransform(topLeft, topRight,
				bottomLeft, alignmentPattern, dimension);
		return sampleGrid(image, transform, dimension);
	}

	private ImageTransform createTransform(PatternPoint topLeft,
			PatternPoint topRight, PatternPoint bottomLeft,
			PatternPoint alignmentPattern, int dimension) {
		float dimMinusThree = (float) dimension - 3.5f;
		float bottomRightX;
		float bottomRightY;
		float sourceBottomRightX;
		float sourceBottomRightY;
		if (alignmentPattern != null) {
			bottomRightX = alignmentPattern.getX();
			bottomRightY = alignmentPattern.getY();
			sourceBottomRightX = dimMinusThree - 3.0f;
			sourceBottomRightY = sourceBottomRightX;
		} else {
			bottomRightX = (topRight.getX() - topLeft.getX())
					+ bottomLeft.getX();
			bottomRightY = (topRight.getY() - topLeft.getY())
					+ bottomLeft.getY();
			sourceBottomRightX = dimMinusThree;
			sourceBottomRightY = dimMinusThree;
		}

		return ImageTransform.quadrilateralToQuadrilateral(3.5f, 3.5f,
				dimMinusThree, 3.5f, sourceBottomRightX, sourceBottomRightY,
				3.5f, dimMinusThree, topLeft.getY(), topLeft.getX(),
				topRight.getY(), topRight.getX(), bottomRightY, bottomRightX,
				bottomLeft.getY(), bottomLeft.getX());
	}

	private BitMatrix sampleGrid(BinaryImage image, ImageTransform transform,
			int dimension) throws QRCodeException {
		QRCodeSample codeSample = new QRCodeSample();
		BitMatrix bitMatrix = codeSample.sampleGrid(image, dimension,
				dimension, transform);
		return bitMatrix;
	}

	protected final PatternPoint findAlignmentInRegion(
			float overallEstModuleSize, int estAlignmentX, int estAlignmentY)
			throws QRCodeException {
		int allowance = (int) (4.0f * overallEstModuleSize);
		int alignmentAreaLeftX = Math.max(0, estAlignmentX - allowance);
		int alignmentAreaRightX = Math.min(image.getHeight() - 1, estAlignmentX
				+ allowance);
		if (alignmentAreaRightX - alignmentAreaLeftX < overallEstModuleSize * 3) {
			throw new QRCodeException(INVALID_REGION);
		}

		int alignmentAreaTopY = Math.max(0, estAlignmentY - allowance);
		int alignmentAreaBottomY = Math.min(image.getWidth() - 1, estAlignmentY
				+ allowance);
		if (alignmentAreaBottomY - alignmentAreaTopY < overallEstModuleSize * 3) {
			throw new QRCodeException(INVALID_REGION);
		}

		AlignmentPatternFinder alignmentFinder = new AlignmentPatternFinder(
				image, alignmentAreaLeftX, alignmentAreaTopY,
				alignmentAreaRightX - alignmentAreaLeftX, alignmentAreaBottomY
						- alignmentAreaTopY, overallEstModuleSize);
		PatternPoint alignmentPattern = alignmentFinder.find();
		if (alignmentPattern == null) {
			throw new QRCodeException(QR_CODE_NOT_FOUND);
		}
		return alignmentPattern;
	}

	protected final float calculateModuleSize(PatternPoint topLeft,
			PatternPoint topRight, PatternPoint bottomLeft) {
		return (calculateModuleSizeOneWay(topLeft, topRight) + calculateModuleSizeOneWay(
				topLeft, bottomLeft)) / 2.0f;
	}

	private static int determineVersion(PatternPoint topLeft,
			PatternPoint topRight, PatternPoint bottomLeft, float moduleSize)
			throws QRCodeException {
		float tltrCentersDimension = MathUtils.getDistance(topLeft, topRight);
		float tlblCentersDimension = MathUtils.getDistance(topLeft, bottomLeft);
		float averageDimension = (tltrCentersDimension + tlblCentersDimension) / 2;

		int provisionallyVersion = (Math.round(averageDimension / moduleSize) - 10) / 4;
		if (provisionallyVersion < 1) {
			throw new QRCodeException(INVALID_VERSION);
		}
		return provisionallyVersion;
	}

	private float calculateModuleSizeOneWay(PatternPoint pattern,
			PatternPoint otherPattern) {
		float moduleSizeEst1 = sizeOfBlackWhiteBlackRunBothWays(pattern.getX(),
				pattern.getY(), otherPattern.getX(), otherPattern.getY());
		float moduleSizeEst2 = sizeOfBlackWhiteBlackRunBothWays(
				otherPattern.getX(), otherPattern.getY(), pattern.getX(),
				pattern.getY());
		if (Float.isNaN(moduleSizeEst1)) {
			return moduleSizeEst2 / 7.0f;
		}
		if (Float.isNaN(moduleSizeEst2)) {
			return moduleSizeEst1 / 7.0f;
		}
		return (moduleSizeEst1 + moduleSizeEst2) / 14.0f;
	}

	private float sizeOfBlackWhiteBlackRunBothWays(float x0, float y0,
			float x1, float y1) {
		float result = sizeOfBlackWhiteBlackRun((int) x0, (int) y0, (int) x1,
				(int) y1);
		// The general equation of the line through the endpoints.
		float scale = 1.0f;
		float x = x0 - (x1 - x0);
		if (x < 0) {
			scale = (float) x0 / (float) (x0 - x);
			x = 0;
		} else if (x >= image.getWidth()) {
			scale = (float) (image.getWidth() - 1 - x0) / (float) (x - x0);
			x = image.getWidth() - 1;
		}
		float y = (y0 - (y1 - y0) * scale);
		scale = 1.0f;
		if (y < 0) {
			scale = (float) y0 / (float) (y0 - y);
			y = 0;
		} else if (y >= image.getHeight()) {
			scale = (float) (image.getHeight() - 1 - y0) / (float) (y - y0);
			y = image.getHeight() - 1;
		}
		x = (x0 + (x - x0) * scale);
		// ---------------------------------------------------------------
		result += sizeOfBlackWhiteBlackRun((int) x0, (int) y0, (int) x, (int) y);
		return result - 1.0f;
	}

	private float sizeOfBlackWhiteBlackRun(int x0, int y0, int x1, int y1) {
		// see more http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
		boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
		if (steep) {
			int temp = x0;
			x0 = y0;
			y0 = temp;
			temp = x1;
			x1 = y1;
			y1 = temp;
		}

		int dx = Math.abs(x1 - x0);
		int dy = Math.abs(y1 - y0);
		int error = -dx >> 1;
		int xstep = x0 < x1 ? 1 : -1;
		int ystep = y0 < y1 ? 1 : -1;

		int state = 0;
		int xLimit = x1 + xstep;
		for (int x = x0, y = y0; x != xLimit; x += xstep) {
			int realX = steep ? y : x;
			int realY = steep ? x : y;
			if ((state == 1) == image.getPixel(realX, realY)) {
				if (state == 2) {
					return MathUtils.getDistance(x, y, x0, y0);
				}
				state++;
			}

			error += dy;
			if (error > 0) {
				if (y == y1) {
					break;
				}
				y += ystep;
				error -= dx;
			}
		}
		if (state == 2) {
			return MathUtils.getDistance(x1 + xstep, y1, x0, y0);
		}
		return Float.NaN;
	}

}
